package cagt.tools.standard.poker.holdem;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import cagt.tools.LimitedHashSet;
import cagt.tools.probability.SubsetsSelector;
import cagt.tools.standard.Color;
import cagt.tools.standard.Figure;
import cagt.tools.standard.StandardCard;
import cagt.tools.standard.poker.Trick;
import cagt.tools.standard.poker.TrickChecker;

/**
 * Class which on basis of table and
 * 
 * @author konrad
 * 
 */
public class TrickProbabilityCounter {

	private Table table;
	private int tableHash;
	private Hand hand;
	private int handHash;
	private Map<Trick, Double> tricks;

	public TrickProbabilityCounter(final Table table) {
		this(table, null);
	}

	public TrickProbabilityCounter(final Table table, final Hand hand) {
		if (table == null) {
			throw new NullPointerException();
		}
		this.table = table;
		this.hand = hand;
	}

	public boolean isTableMode() {
		return hand == null;
	}

	public Map<Trick, Double> getTrickProbabilities() {
		if (!table.isValid() || (!isTableMode() && !hand.isValid())) {
			throw new IllegalStateException();
		}
		// If current hash is no different than before - return result
		int curTableHash = table.hashCode();
		int curHandHash = !isTableMode() ? hand.hashCode() : 0;
		if (curTableHash == tableHash && curHandHash == handHash) {
			return tricks;
		}

		// Prepare set of all known cards
		Set<StandardCard> knownCards = new HashSet<StandardCard>();
		knownCards.addAll(table);
		if (!isTableMode()) {
			knownCards.addAll(hand);
		}

		// Prepare result
		tricks = new HashMap<Trick, Double>();

		// Prepare set of cards not known
		Set<StandardCard> unknownCards = getDeckComplacement(knownCards);

		// Adjust the set of known cards by boguses (red and black jokers).
		final int requiredSize = Table.TABLE_LIMIT
				+ (isTableMode() ? 0 : Hand.HAND_LIMIT);
		while (knownCards.size() < requiredSize) {
			knownCards.add(new StandardCard(Color.values()[requiredSize
					- knownCards.size()], Figure.Joker));
		}

		// Prepare selector of cards
		SubsetsSelector<StandardCard> knownCardSelector = new SubsetsSelector<StandardCard>(
				knownCards, Table.TABLE_LIMIT);
		while (knownCardSelector.hasNext()) {
			// Get the subset card selector
			Set<StandardCard> knownCardSubset = knownCardSelector.next();

			// Remove boguses
			Iterator<StandardCard> knownCardSubsetIterator = knownCardSubset
					.iterator();
			while (knownCardSubsetIterator.hasNext()) {
				if (knownCardSubsetIterator.next().getFigure() == Figure.Joker) {
					knownCardSubsetIterator.remove();
				}
			}

			// Count probability for this set
			Map<Trick, Double> probForSubset = probabilityOfTricks(
					knownCardSubset, unknownCards);
			if (probForSubset != null) {
				for (Map.Entry<Trick, Double> entry : probForSubset.entrySet()) {
					Double actualProb = tricks.get(entry.getKey());
					// If actual probability is unknown or is lower than entry
					// value, substitue it
					if (actualProb == null || actualProb < entry.getValue()) {
						actualProb = entry.getValue();
					}
					tricks.put(entry.getKey(), actualProb);
				}
			}
		}

		// Remember hashes
		tableHash = curTableHash;
		handHash = curHandHash;
		return tricks;
	}

	public static Map<Trick, Double> probabilityOfTricks(
			Set<StandardCard> cardsOwned, Set<StandardCard> cardsPoolToDraw) {
		// Prepare counting
		Map<Trick, Integer> tricksSum = new HashMap<Trick, Integer>();
		for (Trick trick : Trick.values()) {
			tricksSum.put(trick, 0);
		}
		// Run recurrence
		LimitedHashSet<StandardCard> cardsOwnedAsATable = new LimitedHashSet<StandardCard>(
				Table.TABLE_LIMIT);
		cardsOwnedAsATable.addAll(cardsOwned);
		count(tricksSum, new TrickChecker(cardsOwnedAsATable), cardsPoolToDraw);
		// Return result;
		int sum = 0;
		for (Integer val : tricksSum.values()) {
			sum += val;
		}
		// Create the result
		Map<Trick, Double> result = new HashMap<Trick, Double>();
		for (Map.Entry<Trick, Integer> trick : tricksSum.entrySet()) {
			result.put(trick.getKey(), (double) trick.getValue() / sum);
		}
		return result;
	}

	private static void count(Map<Trick, Integer> result,
			TrickChecker trickChecker, Set<StandardCard> cardsPoolToDraw) {
		// Stop term - download the trick and put it to the result
		if (trickChecker.getCards().isFull()) {
			// Count the trick
			Trick trick = trickChecker.getTrick();
			// Add the stats
			Integer stat = result.get(trick);
			stat = (stat == null ? 0 : stat) + 1;
			result.put(trick, stat);
			return;
		}
		Set<StandardCard> cardPoolCopy = new HashSet<StandardCard>(
				cardsPoolToDraw);
		// Otherwise for each card
		for (StandardCard card : cardsPoolToDraw) {
			trickChecker.getCards().add(card);
			cardPoolCopy.remove(card);
			count(result, trickChecker, cardPoolCopy);
			cardPoolCopy.add(card);
			trickChecker.getCards().remove(card);
		}
	}

	/**
	 * Returns the complacement of cards from the deck.
	 * 
	 * @param knownCards
	 *            Cards collection, which complacement should be generated.
	 * @return Complacement.
	 */
	public static Set<StandardCard> getDeckComplacement(
			Collection<StandardCard> knownCards) {
		Set<StandardCard> deck = new HashSet<StandardCard>(
				Rules.getHoldemDeck());
		deck.removeAll(knownCards);
		return deck;
	}

}
